#!/bin/sh

## Updater für die user.js von Privacy-Handbuch.de
## Version: 1.4.0
## by: TotallyLeGIT

## based on the ghacks user.js updater by:
## Author: Pat Johnson (@overdodactyl)
## Additional contributors: @earthlng, @ema-pe, @claustromaniac

## Dependencies: ·curl or wget


#########################
# Misc/Helper Functions #
#########################

# sets variables based on the operating system reported by uname
abstract_os() {
  if [ "$(uname)" = 'Darwin' ]; then
    readonly open_command='open'
    readonly ff_file_path="${HOME}/Library/Application Support/Firefox/profiles.ini"
    readonly tb_file_path="${HOME}/Library/Thunderbird/profiles.ini"
    readonly ff_command='/Applications/Firefox.app/Contents/MacOS/firefox --version 2>/dev/null'
  else
    readonly open_command='xdg-open'
    readonly ff_file_path="${HOME}/.mozilla/firefox/profiles.ini"
    readonly tb_file_path="${HOME}/.thunderbird/profiles.ini"
    readonly ff_command='firefox --version 2>/dev/null'
  fi
  # set effective path for each profile.ini
  [ -f "${ff_file_path}" ] && ff_file="${ff_file_path}" || ff_file=''
  [ -f "${tb_file_path}" ] && tb_file="${tb_file_path}" || tb_file=''
  readonly ff_file tb_file
}

# parses command line options and sets variables accordingly
parse_options() {
  if [ "${#}" -ne 0 ]; then
    # display usage if first argument contains -help
    if printf '%s' "${1}" | grep -qi '\-help'; then
      usage
      exit 0
    else
      while getopts ':abc:de:hilno:p:rsuvy' opt; do
        case "${opt}" in
          a)
            profile_path_option='all'
            ;;
          b)
            backup='single'
            ;;
          c)
            if ! config_wanted="$(printf '%s' "${OPTARG}" \
              | tr '[:upper:]' '[:lower:]' \
              | awk '{print $1; exit}' \
              | grep '^minimal$\|^moderat$\|^streng$\|^hotspot$\|^tb$')"
            then
              printf '%s' "${red}Error: Invalid CONFIG.${neutral} Valid values"
              printf '%s\n' " are: minimal, moderat, streng, hotspot or tb"
              exit 1
            fi
            ;;
          d)
            compare='true'
            ;;
          e)
            if ff_version_wanted="$(printf '%s' "${OPTARG}" \
                                    | awk '{print $1; exit}' \
                                    | grep '^[[:digit:]]*$')"
            then
              ff_version_specified='true'
            else
              printf '%s\n' "${red}Error: VERSION must be an integer.${neutral}"
              exit 1
            fi
            ;;
          h)
            usage
            exit 0
            ;;
          i)
            view='true'
            ;;
          l)
            profile_path_option='list'
            ;;
          n)
            override='false'
            ;;
          o)
            override='true'
            override_path="${OPTARG}"
            ;;
          p)
            update_profile_path "${OPTARG}"
            ;;
          r)
            just_read='true'
            ;;
          # TODO: remove '-s' in future version
          s|u)
            update='true'
            ;;
          v)
            printf '%s\n' "PH-userjs-updater ${version}"
            exit 0
            ;;
          y)
            confirm='no'
            ;;
          \?)
            printf '%s' "${red}Error! Invalid option:" 1>&2
            printf '%s\n' " -${OPTARG}${neutral}"      1>&2
            usage
            exit 2
            ;;
          :)
            printf '%s' "${red}Error! Option -${OPTARG}"     1>&2
            printf '%s\n' " requires an argument.${neutral}" 1>&2
            usage
            exit 2
            ;;
        esac
      done
    fi
  fi
}

# initialize misc stuff
# expects one argument: this script's name (usually $0)
prepare() {
  tmp_dir="$(mktemp -d)"
  readonly tmp_dir
  trap 'exit 130' INT HUP QUIT TERM ALRM
  # shellcheck disable=SC2154
  trap 'rc="${?}"; rm -rf "${tmp_dir}"; exit "${rc}"' EXIT

  script_path="$(readlink -f "${1}" 2>/dev/null \
    || greadlink -f "${1}" 2>/dev/null || printf '%s' "${1}")"
  readonly script_path

  # Download method priority: curl > wget
  if command -v curl >/dev/null; then
    download_command='curl --max-redirs 2 --max-time 30 --fail --silent --output'
  elif command -v wget >/dev/null; then
    # -O file = --output-document=file
    download_command='wget --max-redirect=2 --timeout=30 --quiet -O'
  else
    printf '%s\n' \
      "${red}This script requires curl or wget. Process aborted.${neutral}"
    exit 2
  fi
}

# determines firefox update channel
# returns 0 for release or the major version number for esr
set_firefox_version() {
  if [ "${ff_version_specified}" = 'false' ]; then
    if ff_version_wanted="$(eval "${ff_command}")"; then
      if printf '%s' "${ff_version_wanted}" | grep -q 'esr'; then
        ff_version_wanted="$(printf '%s' "${ff_version_wanted}" \
                              | cut -d ' ' -f 3 | cut -d '.' -f 1)"
      else
        ff_version_wanted='0'
      fi
    else
      printf '%s' "${yellow}Warning: No firefox executable in PATH."
      printf '%s\n' " Assuming updapte channel 'release'.${neutral}"
      ff_version_wanted='0'
    fi
  fi
  [ "${ff_version_wanted}" -eq 0 ] && esr_string='release' \
    || esr_string="ESR ${ff_version_wanted}"
  printf '%s\n' "Firefox variant:  ${green}${esr_string}${neutral}"
}

# initialize variables needed for parse_options()
set_variables() {
  # Colors used for printing only when stdout is a terminal
  if [ -t 1 ]; then
      red="$(tput setaf 1)$(tput bold)"
      blue="$(tput setaf 4)"
      green="$(tput setaf 2)"
      yellow="$(tput setaf 3)"
      # No Color
      neutral="$(tput sgr0)"
  else
      red=''
      blue=''
      green=''
      yellow=''
      neutral=''
  fi

  # Argument defaults
  backup='multiple'
  compare='false'
  config_wanted='keep'
  confirm='yes'
  ff_version_specified='false'
  just_read='false'
  # soft: look for user-overrides.js but only give info when none found
  override='soft'
  override_path='user-overrides.js'
  profile_path_option='false'
  pwd_start="$(pwd)"
  update='false'
  version='1.3.1'
  view='false'

  # constants
  readonly backup_dir='userjs_backups'
  readonly diff_dir='userjs_diffs'
}

show_banner() {
  printf '%s%s\n\n' "${blue}" \
"┌───────────────────────────────────────────────┐
│    Updater for Privacy-Handbuch's user.js     │
│   suitable for both Firefox and Thunderbird   │
└───────────────────────────────────────────────┘${neutral}
Updater:             ${blue}https://notabug.org/TotallyLeGIT/PH-userjs-updater${neutral}
Firefox user.js:     ${blue}https://www.privacy-handbuch.de/handbuch_21u.htm${neutral}
Thunderbird user.js: ${blue}https://www.privacy-handbuch.de/handbuch_31p.htm${neutral}"
}

usage() {
show_banner
  printf '%s' "${blue}Usage: ${0} [-abdhilnrsuvy] [-c CONFIG]"
  printf '%s\n\n'  " [-e VERSION] [-o OVERRIDE] [-p PROFILE]${neutral}

  -a          Update all Firefox and Thunderbird profiles at once while keeping
              CONFIGs the same. Profiles without a user.js will be skipped.
  -b          Only keep one user.js backup and one diff file.
  -c CONFIG   Specify the Firefox user.js config you want: minimal, moderat, streng or hotspot
              or for Thunderbird: tb .
              This is only needed if you want to change to another config.
              Note that this option will always have effect on all selected profiles.
  -d          Create a diff file comparing old and new user.js within ${diff_dir} subdirectory.
  -e VERSION  Force download of user.js for Firefox ESR with version number VERSION.
              Make sure the user.js for this version actually exists.
              Example: '-e 91' will download the user.js for Firefox ESR Version 91.x .
              Use '-e 0' to force the download of the user.js for the release update channel.
              Precedence to find Firefox update channel:
                '-e' option > read from user.js > 'firefox --version' > release (fallback)
  -h          Show this help message and exit.
  -i          Inspect the resulting user.js file.
  -l          Interactively choose your profile from a list.
  -n          Do not append any overrides even if user-overrides.js exists.
  -o OVERRIDE Filename or path to overrides file, needed if different than PROFILE/user-overrides.js .
              If used with -p, the path may be relative to PROFILE or an absolute path.
              If given a directory, all files inside ending in .js will be appended.
  -p PROFILE  Path to your profile directory, needed if different than this script's location.
              May be used multiple times.
              IMPORTANT: If the profile path includes spaces, wrap whole path in quotes.
  -r          Only download user.js to a temporary file and open it. This requires -c.
  -s          (Deprecated.) This is an alias for '-u' until it is removed.
  -u          Check for updates of this updater and install if available.
  -v          Display updater version and exit.
  -y          Update user.js without confirmation."
}

#########################
#  (Pro)File Handling   #
#########################

# downloads a resource. expects two arguments:
# $1 the file name, where the resource will be saved to
# $2 the resource to download (a link)
download_file() {
  [ -f "${1}" ] && return 0
  if ! eval "${download_command} '${1}' '${2}'"; then
    printf '%s\n' "${red}Download of resource failed:${neutral} ${2}"
    return 33
  fi
}

# updates profile_path (especially with list/-l) or exits on error
get_profile_path() {
  if [ "${profile_path_option}" = 'list' ]; then
    if [ -n "${ff_file}" ]; then
      ini="${ff_file}"
      [ -n "${tb_file}" ] && ini='both' || printf 'Firefox detected.\n\n'
    elif [ -n "${tb_file}" ]; then
      ini="${tb_file}"
      printf 'Thunderbird detected.\n\n'
    else
      printf '%s' "${red}Error: Sorry, Thunderbird and Firefox could"
      printf '%s' " not be found or -l is not supported for your OS."
      printf '%s\n' " Try -p option.${neutral}"
      exit 1
    fi

    if [ "${ini}" = 'both' ]; then
      while [ -z "${reply}" ]; do
        printf '(F)irefox and (T)hunderbird were detected. Select one: f,t? '
        read -r reply
      done
      reply="$(printf '%s' "${reply}" | tr '[:upper:]' '[:lower:]')"
      if [ -z "${reply##f*}" ]; then
        ini="${ff_file}"
        printf 'Firefox was selected.\n\n'
      elif [ -z "${reply##t*}" ]; then
        ini="${tb_file}"
        printf 'Thunderbird was selected.\n\n'
      else
        printf '%s' "${red}Error: Could not parse your input.${neutral}"
        exit 1
      fi
    fi
    read_ini_file "${ini}"
  elif [ "${profile_path_option}" = 'all' ]; then
    for file in "${ff_file}" "${tb_file}"; do
      [ -f "${file}" ] \
        && profile_path_temp="$(sed -ne 's/^Path=\(.*\)$/\1/p' "${file}")"
      while read -r line; do
        if ! printf '%s' "${line}" | grep -q '^/'; then
          profile_path_to_update="${file%profiles.ini}${line}"
        else
          profile_path_to_update="${line}"
        fi
        update_profile_path "${profile_path_to_update}"
      # we don't want a subshell here
      done <<EOF
${profile_path_temp}
EOF
    done
  elif [ "${profile_path_option}" = 'false' ]; then
    profile_path="$(dirname "${script_path}")"
  fi
}

# opens a file for the user to view
# expects one argument: file_path
open_file() {
  "${open_command}" "${1}" || printf '%s %s\n' "${red}Error: Sorry," \
    "opening files is not supported for your OS.${neutral}"
}

# parses profiles.ini to find the correct profile
# expects one argument: absolute path of profiles.ini
read_ini_file() {
  readonly ini_file="${1}"

  # only 1 profile found
  if [ "$(grep -c '^\[Profile' "${ini_file}")" -eq 1 ]; then
    ini_content="$(grep '^\[Profile' -A 4 "${ini_file}")"
  # more than 1 profile
  else
    printf 'Profiles found:\n'
    printf '–––––––––––––––––––––––––––––––––––\n'
    grep -E 'Default=[^1]|\[Profile[0-9]*\]|Name=|Path=|^$' "${ini_file}"
    printf '–––––––––––––––––––––––––––––––––––\n'
    printf '%s' \
      "Select the profile number ( Profile(0), Profile(1) etc. ): 1,2,…? "
    read -r reply
    printf '\n'
    ini_content="$(grep '^\[Profile'"${reply}" -A 4 "${ini_file}")" \
      || {  printf '%s\n' "${red}Profile${reply} does not exist!${neutral}"
            exit 1 ; }
  fi

  profile_path_to_add="$(printf '%s' "${ini_content}" \
    | sed -n 's/^Path=\(.*\)$/\1/p')"
  path_is_rel="$(printf '%s' "${ini_content}" \
    | sed -n 's/^IsRelative=\([01]\)$/\1/p')"
  readonly path_is_rel

  # update global variable if path is relative
  [ "${path_is_rel}" -eq 1 ] \
    && profile_path_to_add="$(dirname "${ini_file}")/${profile_path_to_add}"
  update_profile_path "${profile_path_to_add}"
}

# adds a path to the list of profile paths, separated by newlines
# expects one argument: a profile path
update_profile_path() {
  # if profile path empty: update it
  if [ -z "${profile_path}" ]; then
    profile_path="${1}"
    # update profile_path_option var so we know there's a profile path
    [ "${profile_path_option}" = 'false' ] && profile_path_option='true'
  # append additional profile path
  else
    profile_path="$(printf '%s\n%s' "${profile_path}" "${1}")"
  fi
}

#########################
#    Update Updater     #
#########################

# update this updater
# expects $@ as arguments
update_updater() {
  # update check has not been activated
  [ "${update}" = 'false' ] && return 0
  readonly new_updater="${tmp_dir}/new_updater"
  download_file "${new_updater}" \
    'https://notabug.org/TotallyLeGIT/PH-userjs-updater/raw/main/updater' \
    || return 1
  chmod u+x "${new_updater}"
  current_updater_version="$("${script_path}" -v | cut -d ' ' -f 2)"
  new_updater_version="$("${new_updater}" -v | cut -d ' ' -f 2)"
  readonly current_updater_version_const="${current_updater_version}"

  # compare updater versions, if different
  while [ "${current_updater_version}" != "${new_updater_version}" ]; do
    difference="$((${new_updater_version%%.*}-${current_updater_version%%.*}))"
    # newer version available
    if [ "${difference}" -gt 0 ]; then
      # apply update
      mv "${new_updater}" "${script_path}"
      printf 'Updater successfully updated.\n'
      printf '%s' "Changelog:        ${green}https://notabug.org/TotallyLeGIT/"
      printf '%s\n\n' "PH-userjs-updater/raw/main/CHANGELOG.md${neutral}"
      # update user.js with new version
      "${script_path}" "${@}"
      # exit with exit code from updater call above
      exit "${?}"
    # version number is equal (go to next number)
    elif [ "${difference}" -eq 0 ]; then
      current_updater_version="${current_updater_version#*.}"
      new_updater_version="${new_updater_version#*.}"
    # this should not be happening
    else
      printf '%s' "${red}Error: Something with the updater version"
      printf '%s' " seems funny. Check updater homepage"
      printf '%s\n' " and/or update manually.${neutral}"
      return 2
    fi
  done
}

#########################
#    Update user.js     #
#########################

# adds given override file or all files suffixed .js in given directory
# expects one argument: path to a file or directory
add_override() {
  input="${1}"

  if [ -f "${input}" ]; then
    printf '\n' >> 'user.js'
    cat "${input}" >> 'user.js'
    overrides_status="${green}file appended:${neutral} ${input}"
  elif [ -d "${input}" ]; then
    files="$(find "${input}" -mindepth 1 -maxdepth 1 -name '*.js')"
    if [ -z "${files}" ]; then
      overrides_status="${yellow}empty directory:${neutral} ${input}"
    else
      printf '%s\n' "${files}" \
        | while read -r f
      do
        add_override "${f}"
      done
      # skip output because every found file produces one
      return 0
    fi
  else
    if [ "${override}" = 'soft' ]; then
      overrides_status="${green}no overrides found${neutral}"
    else
      overrides_status="${yellow}invalid path:${neutral} ${input}"
    fi
  fi
  printf '%s\n' "overrides status: ${overrides_status}"
}

# returns a user.js download link
# expects one argument: the full current version (e.g.: ff91_streng_16022020)
get_userjs_link() {
  config_is="${1%_*}"
  # will be empty or e. g. 'ff91' afterwards
  esr_is="$(printf '%s' "${config_is}" \
            | sed -ne 's/\(ff[[:digit:]]*\)_.*/\1/p')"
  # will be e. g. 'streng' afterwards
  config_is="${config_is#*_}"
  if [ "${config_wanted}" = 'keep' ]; then
    # 'keep' means to try autodetection, so 'Not detected.' is a problem
    [ "${config_is}" = 'Not detected.' ] && exit 33
    config_wanted="${config_is}"
  fi
  # TODO: remove %%2* in future version (tb2 leftover)
  if [ "${config_wanted%%2*}" != 'tb' ]; then
    # version not specified → try autodetection from user.js
    if [ "${ff_version_specified}" = 'false' ] && [ -n "${esr_is}" ]; then
      esr_url="${esr_is}/"
    # version specified or could not extract from user.js
    else
      [ "${ff_version_wanted}" -eq 0 ] && esr_url='' \
        || esr_url="ff${ff_version_wanted}/"
    fi
  fi

  printf '%s' "https://www.privacy-handbuch.de/download/"
  printf '%s' "${esr_url}${config_wanted}/user.js"
}

# returns the version number of a user.js file
get_userjs_version() {
  [ -e "${1}" ] && userjs_version="$(sed -ne \
    '/user\.js\.prhdb/ s/.*"\(\([[:alnum:]]*_\)*[[:alpha:]]*_[0-9]*\)".*/\1/p' \
    -e '10 q' "${1}")"
  [ -z "${userjs_version}" ] && printf 'Not detected.' && exit 33
  printf '%s' "${userjs_version}"
}

# downloads a user.js, opens it and exits
read_and_exit() {
  [ "${just_read}" = 'false' ] && return 0
  if [ "${config_wanted}" = 'keep' ]; then
    printf '%s' "${red}Error: -r option requires"
    printf '%s\n' " -c option, too.${neutral}"
    exit 1
  else
    userjs_to_read="$(mktemp -dt userjs.XXXX)/user.js"
    readonly userjs_to_read
    download_file "${userjs_to_read}" \
      "$(get_userjs_link "${config_wanted}")" \
      || exit 1
  fi
  printf '%s' "${green}user.js was saved to temporary"
  printf '%s\n' " file:${neutral} ${userjs_to_read}"
  open_file "${userjs_to_read}"
  exit 0
}

# applies chosen version of user.js and any custom overrides
update_userjs() {
  current_version_long="$(get_userjs_version 'user.js')"

  printf '%s\n' "profile path:     ${green}${PWD}${neutral}"
  printf '%s\n' "currently using:  ${green}${current_version_long}${neutral}"

  if ! link="$(get_userjs_link "${current_version_long}")"; then
    if [ "${profile_path_option}" = 'all' ]; then
      printf '%s\n' "${yellow}Skipping profile.${neutral}"
    else
      printf '%s' "${red}Error: Could not determine which config to download."
      printf '%s\n' " Use -c CONFIG and/or -p PROFILE.${neutral}"
    fi
    return 1
  fi
  # prepare extracting CONFIG (e.g. moderat) out of previously generated link
  tmp_link="${link%/*}"
  new_userjs="${tmp_dir}/new_userjs_${tmp_link##*/}"
  download_file "${new_userjs}" "${link}" || exit 1
  if ! new_version_long="$(get_userjs_version "${new_userjs}")"; then
    printf '%s\n' "${red}Error parsing new user.js${neutral}"
    return 1
  fi
  printf '%s\n' "available online: ${green}${new_version_long}${neutral}"
  if [ "${confirm}" = 'yes' ]; then
    printf '\nThis script will update to the latest user.js'
    if [ "${override}" != 'false' ]; then
      printf ' and append custom configurations from user-overrides.js or a specified path'
    fi
    printf '.\n%s' "${red}Continue? (y)es or (n)o: y,n?${neutral} "
    read -r reply </dev/tty
    if ! printf '%s' "${reply}" | grep -q '^[yY]*$'; then
      printf '%s\n' "${yellow}Skipping profile.${neutral}"
      return 1
    fi
  fi

  # backup user.js
  mkdir -p "${backup_dir}"
  [ "${backup}" = 'single' ] && backup_date="$(date -Iseconds)." \
    || backup_date=''
  backup_name="${backup_dir}/${backup_date}user.js"
  [ -f 'user.js' ] && mv 'user.js' "${backup_name}"

  # install newest user.js
  mv "${new_userjs}" 'user.js'
  printf '%s\n' "user.js status:   ${green}backed up and updated${neutral}"

  # apply overrides
  if [ "${override}" != 'false' ]; then
    printf '\n// user-overrides\n' >> 'user.js'
    add_override "${override_path}"
  fi

  # create diff
  if [ "${compare}" = 'true' ]; then
    [ "${backup}" = 'single' ] && diff_date="$(date -Iseconds)." \
      || diff_date=''
    diff_filename="${diff_dir}/${diff_date}diff.txt"
    diffs="$(diff -wBU 0 "${backup_name}" 'user.js')"

    if [ -n "${diffs}" ]; then
      mkdir -p "${diff_dir}"
      printf '%s' "${diffs}" > "${diff_filename}"
      printf '%s' "Status: ${green}A diff file was"
      printf '%s\n' " created:${neutral} ${PWD}/${diff_filename}"
    else
      printf '%s' "${yellow}Your new user.js file appears to be identical."
      printf '%s\n' " No diff file was created.${neutral}"
    fi
  fi
  [ "${view}" = 'true' ] && open_file "${PWD}/user.js"
}

#########################
#        Main()         #
#########################

set_variables
parse_options "${@}"
prepare "${0}"
update_updater "${@}"
abstract_os
# if esr wasn't forced, try to detect it
set_firefox_version
read_and_exit
get_profile_path
while read -r line; do
  printf '————————————————————————————————————————————\n'
  # this enables us to leave relative profile paths as they are
  cd "${pwd_start}" || exit 99
  if ! cd "${line}" 2>/dev/null; then
    printf '%s' "${yellow}Warning: Profile path not found:"
    printf '%s\n' " ${green}${line}${neutral}"
    continue
  fi
  update_userjs
done <<EOF
${profile_path}
EOF
